package com.zondy.mapgis.dockfx.pane.skin;

import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler;
import com.sun.javafx.scene.control.behavior.TabPaneBehavior;
import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
import com.sun.javafx.scene.traversal.Direction;
import com.sun.javafx.scene.traversal.TraversalEngine;
import com.sun.javafx.util.Utils;
import com.zondy.mapgis.dockfx.pane.DockNodeTab;
import javafx.animation.*;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.css.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.TabPane.TabClosingPolicy;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.SwipeEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;

/**
 * @file ContentTabPaneSkin.java
 * @brief 此类拷贝自jfxrt.jar 1.8的com.sun.javafx.scene.control.skin.TabPaneSkin
 *
 * @author CR
 * @date 2020-6-12
 */
public class ContentTabPaneSkin extends BehaviorSkinBase<TabPane, TabPaneBehavior> {
    private static enum TabAnimation {
        NONE,
        GROW
    }

    private enum TabAnimationState {
        SHOWING, HIDING, NONE;
    }

    private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
        @Override
        public CssMetaData<TabPane, TabAnimation> getCssMetaData() {
            return StyleableProperties.OPEN_TAB_ANIMATION;
        }

        @Override
        public Object getBean() {
            return ContentTabPaneSkin.this;
        }

        @Override
        public String getName() {
            return "openTabAnimation";
        }
    };

    private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
        @Override
        public CssMetaData<TabPane, TabAnimation> getCssMetaData() {
            return StyleableProperties.CLOSE_TAB_ANIMATION;
        }

        @Override
        public Object getBean() {
            return ContentTabPaneSkin.this;
        }

        @Override
        public String getName() {
            return "closeTabAnimation";
        }
    };

    private static int getRotation(Side pos) {
        switch (pos) {
            case TOP:
                return 0;
            case BOTTOM:
                return 180;
            case LEFT:
                return -90;
            case RIGHT:
                return 90;
            default:
                return 0;
        }
    }

    private static Node clone(Node n) {
        if (n == null) {
            return null;
        }

        if (n instanceof ImageView) {
            ImageView iv = (ImageView) n;
            ImageView imageview = new ImageView();
            imageview.setImage(iv.getImage());
            return imageview;
        }

        if (n instanceof Label) {
            Label l = (Label) n;
            Label label = new Label(l.getText(), l.getGraphic());
            return label;
        }
        return null;
    }

    private static final double ANIMATION_SPEED = 150;
    private static final int SPACER = 10;

    private TabHeaderArea tabHeaderArea;
    private ObservableList<TabContentRegion> tabContentRegions;
    private Rectangle clipRect;
    private Rectangle tabHeaderAreaClipRect;
    private Tab selectedTab;
    private boolean isSelectingTab;

    public ContentTabPaneSkin(TabPane tabPane) {
        super(tabPane, new TabPaneBehavior(tabPane));

        clipRect = new Rectangle(tabPane.getWidth(), tabPane.getHeight());
        getSkinnable().setClip(clipRect);

        tabContentRegions = FXCollections.observableArrayList();

        for (Tab tab : getSkinnable().getTabs()) {
            addTabContent(tab);
        }

        tabHeaderAreaClipRect = new Rectangle();
        tabHeaderArea = new TabHeaderArea();
        tabHeaderArea.setClip(tabHeaderAreaClipRect);
        getChildren().add(tabHeaderArea);
        if (getSkinnable().getTabs().size() == 0) {
            tabHeaderArea.setVisible(false);
        }

        initializeTabListener();

        registerChangeListener(tabPane.getSelectionModel().selectedItemProperty(), "SELECTED_TAB");
        registerChangeListener(tabPane.sideProperty(), "SIDE");
        registerChangeListener(tabPane.widthProperty(), "WIDTH");
        registerChangeListener(tabPane.heightProperty(), "HEIGHT");

        selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
        if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) {
            getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex());
            selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
        }
        if (selectedTab == null) {
            getSkinnable().getSelectionModel().selectFirst();
        }
        selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
        isSelectingTab = false;

        initializeSwipeHandlers();
    }

    public StackPane getSelectedTabContentRegion() {
        for (TabContentRegion contentRegion : tabContentRegions) {
            if (contentRegion.getTab().equals(selectedTab)) {
                return contentRegion;
            }
        }
        return null;
    }

    @Override
    protected void handleControlPropertyChanged(String property) {
        super.handleControlPropertyChanged(property);
        if ("SELECTED_TAB".equals(property)) {
            isSelectingTab = true;
            selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
            getSkinnable().requestLayout();
        } else if ("SIDE".equals(property)) {
            updateTabPosition();
        } else if ("WIDTH".equals(property)) {
            clipRect.setWidth(getSkinnable().getWidth());
        } else if ("HEIGHT".equals(property)) {
            clipRect.setHeight(getSkinnable().getHeight());
        }
    }

    private void removeTabs(List<? extends Tab> removedList) {
        for (final Tab tab : removedList) {
            stopCurrentAnimation(tab);
            final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
            if (tabRegion != null) {
                tabRegion.isClosing = true;

                tabRegion.removeListeners(tab);
                removeTabContent(tab);

                ContextMenu popupMenu = tabHeaderArea.controlButtons.popup;
                TabMenuItem tabItem = null;
                if (popupMenu != null) {
                    for (MenuItem item : popupMenu.getItems()) {
                        tabItem = (TabMenuItem) item;
                        if (tab == tabItem.getTab()) {
                            break;
                        }
                        tabItem = null;
                    }
                }
                if (tabItem != null) {
                    tabItem.dispose();
                    popupMenu.getItems().remove(tabItem);
                }

                EventHandler<ActionEvent> cleanup = ae ->
                {
                    tabRegion.animationState = TabAnimationState.NONE;
                    tabHeaderArea.removeTab(tab);
                    tabHeaderArea.requestLayout();
                    if (getSkinnable().getTabs().isEmpty()) {
                        tabHeaderArea.setVisible(false);
                    }
                };

                if (closeTabAnimation.get() == TabAnimation.GROW) {
                    tabRegion.animationState = TabAnimationState.HIDING;
                    Timeline closedTabTimeline = tabRegion.currentAnimation = createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 0.0F, cleanup);
                    closedTabTimeline.play();
                } else {
                    cleanup.handle(null);
                }
            }
        }
    }

    private void stopCurrentAnimation(Tab tab) {
        final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
        if (tabRegion != null) {
            Timeline timeline = tabRegion.currentAnimation;
            if (timeline != null && timeline.getStatus() == Animation.Status.RUNNING) {
                timeline.getOnFinished().handle(null);
                timeline.stop();
                tabRegion.currentAnimation = null;
            }
        }
    }

    private void addTabs(List<? extends Tab> addedList, int from) {
        int i = 0;
        List<Node> headers = new ArrayList<>(tabHeaderArea.headersRegion.getChildren());
        for (Node n : headers) {
            TabHeaderSkin header = (TabHeaderSkin) n;
            if (header.animationState == TabAnimationState.HIDING) {
                stopCurrentAnimation(header.tab);
            }
        }
        for (final Tab tab : addedList) {
            stopCurrentAnimation(tab);
            if (!tabHeaderArea.isVisible()) {
                tabHeaderArea.setVisible(true);
            }
            int index = from + i++;
            tabHeaderArea.addTab(tab, index);
            addTabContent(tab);
            final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
            if (tabRegion != null) {
                if (openTabAnimation.get() == TabAnimation.GROW) {
                    tabRegion.animationState = TabAnimationState.SHOWING;
                    tabRegion.animationTransition.setValue(0.0);
                    tabRegion.setVisible(true);
                    tabRegion.currentAnimation = createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 1.0, event ->
                    {
                        tabRegion.animationState = TabAnimationState.NONE;
                        tabRegion.setVisible(true);
                        tabRegion.inner.requestLayout();
                    });
                    tabRegion.currentAnimation.play();
                } else {
                    tabRegion.setVisible(true);
                    tabRegion.inner.requestLayout();
                }
            }
        }
    }

    private void initializeTabListener() {
        getSkinnable().getTabs().addListener((ListChangeListener<Tab>) c ->
        {
            List<Tab> tabsToRemove = new ArrayList<>();
            List<Tab> tabsToAdd = new ArrayList<>();
            int insertPos = -1;

            while (c.next()) {
                if (c.wasPermutated()) {
                    TabPane tabPane = getSkinnable();
                    List<Tab> tabs = tabPane.getTabs();

                    int size = c.getTo() - c.getFrom();
                    Tab selTab = tabPane.getSelectionModel().getSelectedItem();
                    List<Tab> permutatedTabs = new ArrayList<Tab>(size);
                    getSkinnable().getSelectionModel().clearSelection();

                    TabAnimation prevOpenAnimation = openTabAnimation.get();
                    TabAnimation prevCloseAnimation = closeTabAnimation.get();
                    openTabAnimation.set(TabAnimation.NONE);
                    closeTabAnimation.set(TabAnimation.NONE);
                    for (int i = c.getFrom(); i < c.getTo(); i++) {
                        permutatedTabs.add(tabs.get(i));
                    }

                    removeTabs(permutatedTabs);
                    addTabs(permutatedTabs, c.getFrom());
                    openTabAnimation.set(prevOpenAnimation);
                    closeTabAnimation.set(prevCloseAnimation);
                    getSkinnable().getSelectionModel().select(selTab);
                }

                if (c.wasRemoved()) {
                    tabsToRemove.addAll(c.getRemoved());
                }

                if (c.wasAdded()) {
                    tabsToAdd.addAll(c.getAddedSubList());
                    insertPos = c.getFrom();
                }
            }

            tabsToRemove.removeAll(tabsToAdd);
            removeTabs(tabsToRemove);

            if (!tabsToAdd.isEmpty()) {
                for (TabContentRegion tabContentRegion : tabContentRegions) {
                    Tab tab = tabContentRegion.getTab();
                    TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab);
                    if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) {
                        tabsToAdd.remove(tabContentRegion.getTab());
                    }
                }
                addTabs(tabsToAdd, insertPos == -1 ? tabContentRegions.size() : insertPos);
            }

            getSkinnable().requestLayout();
        });
    }

    private void addTabContent(Tab tab) {
        TabContentRegion tabContentRegion = new TabContentRegion(tab);
        tabContentRegion.setClip(new Rectangle());
        tabContentRegions.add(tabContentRegion);
        getChildren().add(0, tabContentRegion);
    }

    private void removeTabContent(Tab tab) {
        for (TabContentRegion contentRegion : tabContentRegions) {
            if (contentRegion.getTab().equals(tab)) {
                contentRegion.removeListeners(tab);
                getChildren().remove(contentRegion);
                tabContentRegions.remove(contentRegion);
                break;
            }
        }
    }

    private void updateTabPosition() {
        tabHeaderArea.setScrollOffset(0.0F);
        getSkinnable().applyCss();
        getSkinnable().requestLayout();
    }

    private Timeline createTimeline(final TabHeaderSkin tabRegion, final Duration duration, final double endValue, final EventHandler<ActionEvent> func) {
        Timeline timeline = new Timeline();
        timeline.setCycleCount(1);

        KeyValue keyValue = new KeyValue(tabRegion.animationTransition, endValue, Interpolator.LINEAR);
        timeline.getKeyFrames().clear();
        timeline.getKeyFrames().add(new KeyFrame(duration, keyValue));

        timeline.setOnFinished(func);
        return timeline;
    }

    private boolean isHorizontal() {
        Side tabPosition = getSkinnable().getSide();
        return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition);
    }

    private void initializeSwipeHandlers() {
        if (IS_TOUCH_SUPPORTED) {
            getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> getBehavior().selectNextTab());

            getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> getBehavior().selectPreviousTab());
        }
    }

    private boolean isFloatingStyleClass() {
        return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING);
    }

    private double maxw = 0.0d;

    @Override
    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        for (TabContentRegion contentRegion : tabContentRegions) {
            maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
        }

        final boolean isHorizontal = isHorizontal();
        final double tabHeaderAreaSize = snapSize(isHorizontal ? tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1));

        double prefWidth = isHorizontal ? Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize;
        return snapSize(prefWidth) + rightInset + leftInset;
    }

    private double maxh = 0.0d;

    @Override
    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        for (TabContentRegion contentRegion : tabContentRegions) {
            maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
        }

        final boolean isHorizontal = isHorizontal();
        final double tabHeaderAreaSize = snapSize(isHorizontal ? tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1));

        double prefHeight = isHorizontal ? maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize);
        return snapSize(prefHeight) + topInset + bottomInset;
    }

    @Override
    public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
        Side tabPosition = getSkinnable().getSide();
        if (tabPosition == Side.TOP) {
            return tabHeaderArea.getBaselineOffset() + topInset;
        }
        return 0;
    }

    @Override
    protected void layoutChildren(final double x, final double y, final double w, final double h) {
        TabPane tabPane = getSkinnable();
        Side tabPosition = tabPane.getSide();

        double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
        double tabsStartX = tabPosition.equals(Side.RIGHT) ? x + w - headerHeight : x;
        double tabsStartY = tabPosition.equals(Side.BOTTOM) ? y + h - headerHeight : y;

        if (tabPosition == Side.TOP) {
            tabHeaderArea.resize(w, headerHeight);
            tabHeaderArea.relocate(tabsStartX, tabsStartY);
            tabHeaderArea.getTransforms().clear();
            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP)));
        } else if (tabPosition == Side.BOTTOM) {
            tabHeaderArea.resize(w, headerHeight);
            tabHeaderArea.relocate(w, tabsStartY - headerHeight);
            tabHeaderArea.getTransforms().clear();
            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight));
        } else if (tabPosition == Side.LEFT) {
            tabHeaderArea.resize(h, headerHeight);
            tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight);
            tabHeaderArea.getTransforms().clear();
            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight));
        } else if (tabPosition == Side.RIGHT) {
            tabHeaderArea.resize(h, headerHeight);
            tabHeaderArea.relocate(tabsStartX, y - headerHeight);
            tabHeaderArea.getTransforms().clear();
            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight));
        }

        tabHeaderAreaClipRect.setX(0);
        tabHeaderAreaClipRect.setY(0);
        if (isHorizontal()) {
            tabHeaderAreaClipRect.setWidth(w);
        } else {
            tabHeaderAreaClipRect.setWidth(h);
        }
        tabHeaderAreaClipRect.setHeight(headerHeight);

        double contentStartX = 0;
        double contentStartY = 0;

        if (tabPosition == Side.TOP) {
            contentStartX = x;
            contentStartY = y + headerHeight;
            if (isFloatingStyleClass()) {
                contentStartY -= 1;
            }
        } else if (tabPosition == Side.BOTTOM) {
            contentStartX = x;
            contentStartY = y;
            if (isFloatingStyleClass()) {
                contentStartY = 1;
            }
        } else if (tabPosition == Side.LEFT) {
            contentStartX = x + headerHeight;
            contentStartY = y;
            if (isFloatingStyleClass()) {
                contentStartX -= 1;
            }
        } else if (tabPosition == Side.RIGHT) {
            contentStartX = x;
            contentStartY = y;
            if (isFloatingStyleClass()) {
                contentStartX = 1;
            }
        }

        double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
        double contentHeight = h - (isHorizontal() ? headerHeight : 0);

        for (int i = 0, max = tabContentRegions.size(); i < max; i++) {
            TabContentRegion tabContent = tabContentRegions.get(i);

            tabContent.setAlignment(Pos.TOP_LEFT);
            if (tabContent.getClip() != null) {
                ((Rectangle) tabContent.getClip()).setWidth(contentWidth);
                ((Rectangle) tabContent.getClip()).setHeight(contentHeight);
            }

            tabContent.resize(contentWidth, contentHeight);
            tabContent.relocate(contentStartX, contentStartY);
        }
    }


    private static class StyleableProperties {
        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

        private final static CssMetaData<TabPane, TabAnimation> OPEN_TAB_ANIMATION = new CssMetaData<TabPane, ContentTabPaneSkin.TabAnimation>("-fx-open-tab-animation", new EnumConverter<>(TabAnimation.class), TabAnimation.GROW) {
            @Override
            public boolean isSettable(TabPane node) {
                return true;
            }

            @Override
            public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) {
                ContentTabPaneSkin skin = (ContentTabPaneSkin) node.getSkin();
                return (StyleableProperty<TabAnimation>) skin.openTabAnimation;
            }
        };

        private final static CssMetaData<TabPane, TabAnimation> CLOSE_TAB_ANIMATION = new CssMetaData<TabPane, ContentTabPaneSkin.TabAnimation>("-fx-close-tab-animation", new EnumConverter<>(TabAnimation.class), TabAnimation.GROW) {
            @Override
            public boolean isSettable(TabPane node) {
                return true;
            }

            @Override
            public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) {
                ContentTabPaneSkin skin = (ContentTabPaneSkin) node.getSkin();
                return (StyleableProperty<TabAnimation>) skin.closeTabAnimation;
            }
        };

        static {
            final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
            styleables.add(OPEN_TAB_ANIMATION);
            styleables.add(CLOSE_TAB_ANIMATION);
            STYLEABLES = Collections.unmodifiableList(styleables);
        }
    }

    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }

    @Override
    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
        return getClassCssMetaData();
    }

    class TabHeaderArea extends StackPane {
        private Rectangle headerClip;
        private StackPane headersRegion;
        private StackPane headerBackground;
        private TabControlButtons controlButtons;

        private boolean measureClosingTabs = false;

        private double scrollOffset;

        public TabHeaderArea() {
            getStyleClass().setAll("tab-header-area");
            setManaged(false);
            final TabPane tabPane = getSkinnable();

            headerClip = new Rectangle();

            headersRegion = new StackPane() {
                @Override
                protected double computePrefWidth(double height) {
                    double width = 0.0F;
                    for (Node child : getChildren()) {
                        TabHeaderSkin tabHeaderSkin = (TabHeaderSkin) child;
                        if (tabHeaderSkin.isVisible() && (measureClosingTabs || !tabHeaderSkin.isClosing)) {
                            width += tabHeaderSkin.prefWidth(height);
                        }
                    }
                    return snapSize(width) + snappedLeftInset() + snappedRightInset();
                }

                @Override
                protected double computePrefHeight(double width) {
                    double height = 0.0F;
                    for (Node child : getChildren()) {
                        TabHeaderSkin tabHeaderSkin = (TabHeaderSkin) child;
                        height = Math.max(height, tabHeaderSkin.prefHeight(width));
                    }
                    return snapSize(height) + snappedTopInset() + snappedBottomInset();
                }

                @Override
                protected void layoutChildren() {
                    if (tabsFit()) {
                        setScrollOffset(0.0);
                    } else {
                        if (!removeTab.isEmpty()) {
                            double offset = 0;
                            double w = tabHeaderArea.getWidth() - snapSize(controlButtons.prefWidth(-1)) - firstTabIndent() - SPACER;
                            Iterator<Node> i = getChildren().iterator();
                            while (i.hasNext()) {
                                TabHeaderSkin tabHeader = (TabHeaderSkin) i.next();
                                double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
                                if (removeTab.contains(tabHeader)) {
                                    if (offset < w) {
                                        isSelectingTab = true;
                                    }
                                    i.remove();
                                    removeTab.remove(tabHeader);
                                    if (removeTab.isEmpty()) {
                                        break;
                                    }
                                }
                                offset += tabHeaderPrefWidth;
                            }
//                        } else {
//                            isSelectingTab = true;
                        }
                    }

                    if (isSelectingTab) {
                        ensureSelectedTabIsVisible();
                        isSelectingTab = false;
                    } else {
                        validateScrollOffset();
                    }

                    Side tabPosition = getSkinnable().getSide();
                    double tabBackgroundHeight = snapSize(prefHeight(-1));
                    double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ? snapSize(getWidth()) - getScrollOffset() : getScrollOffset();

                    updateHeaderClip();
                    for (Node node : getChildren()) {
                        TabHeaderSkin tabHeader = (TabHeaderSkin) node;

                        double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get());
                        double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1));
                        tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight);

                        double startY = tabPosition.equals(Side.BOTTOM) ? 0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset();
                        if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) {
                            tabX -= tabHeaderPrefWidth;
                            tabHeader.relocate(tabX, startY);
                        } else {
                            tabHeader.relocate(tabX, startY);
                            tabX += tabHeaderPrefWidth;
                        }
                    }
                }
            };
            headersRegion.getStyleClass().setAll("headers-region");
            headersRegion.setClip(headerClip);

            headerBackground = new StackPane();
            headerBackground.getStyleClass().setAll("tab-header-background");

            int i = 0;
            for (Tab tab : tabPane.getTabs()) {
                addTab(tab, i++);
            }

            controlButtons = new TabControlButtons();
            controlButtons.setVisible(false);
            if (controlButtons.isVisible()) {
                controlButtons.setVisible(true);
            }
            getChildren().addAll(headerBackground, headersRegion, controlButtons);

            addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) ->
            {
                Side side = getSkinnable().getSide();
                side = side == null ? Side.TOP : side;
                switch (side) {
                    default:
                    case TOP:
                    case BOTTOM:
                        setScrollOffset(scrollOffset - e.getDeltaY());
                        break;
                    case LEFT:
                    case RIGHT:
                        setScrollOffset(scrollOffset + e.getDeltaY());
                        break;
                }
            });
        }

        private void updateHeaderClip() {
            Side tabPosition = getSkinnable().getSide();

            double x = 0;
            double y = 0;
            double clipWidth = 0;
            double clipHeight = 0;
            double maxWidth = 0;
            double shadowRadius = 0;
            double clipOffset = firstTabIndent();
            double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1));

            measureClosingTabs = true;
            double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
            measureClosingTabs = false;

            double headersPrefHeight = snapSize(headersRegion.prefHeight(-1));

            if (controlButtonPrefWidth > 0) {
                controlButtonPrefWidth = controlButtonPrefWidth + SPACER;
            }

            if (headersRegion.getEffect() instanceof DropShadow) {
                DropShadow shadow = (DropShadow) headersRegion.getEffect();
                shadowRadius = shadow.getRadius();
            }

            maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset;
            if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) {
                if (headersPrefWidth < maxWidth) {
                    clipWidth = headersPrefWidth + shadowRadius;
                } else {
                    x = headersPrefWidth - maxWidth;
                    clipWidth = maxWidth + shadowRadius;
                }
                clipHeight = headersPrefHeight;
            } else {
                x = -shadowRadius;
                clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius;
                clipHeight = headersPrefHeight;
            }

            headerClip.setX(x);
            headerClip.setY(y);
            headerClip.setWidth(clipWidth);
            headerClip.setHeight(clipHeight);
        }

        private void addTab(Tab tab, int addToIndex) {
            TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab);
            headersRegion.getChildren().add(addToIndex, tabHeaderSkin);
        }

        private List<TabHeaderSkin> removeTab = new ArrayList<>();

        private void removeTab(Tab tab) {
            TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab);
            if (tabHeaderSkin != null) {
                if (tabsFit()) {
                    headersRegion.getChildren().remove(tabHeaderSkin);
                } else {
                    removeTab.add(tabHeaderSkin);
                    tabHeaderSkin.removeListeners(tab);
                }
            }
        }

        private TabHeaderSkin getTabHeaderSkin(Tab tab) {
            for (Node child : headersRegion.getChildren()) {
                TabHeaderSkin tabHeaderSkin = (TabHeaderSkin) child;
                if (tabHeaderSkin.getTab().equals(tab)) {
                    return tabHeaderSkin;
                }
            }
            return null;
        }

        private boolean tabsFit() {
            double headerPrefWidth = snapSize(headersRegion.prefWidth(-1));
            double controlTabWidth = snapSize(controlButtons.prefWidth(-1));
            double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER;
            return visibleWidth < getWidth();
        }

        private void ensureSelectedTabIsVisible() {
            double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight());
            double controlTabWidth = snapSize(controlButtons.getWidth());
            double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER;

            double offset = 0.0;
            double selectedTabOffset = 0.0;
            double selectedTabWidth = 0.0;
            for (Node node : headersRegion.getChildren()) {
                TabHeaderSkin tabHeader = (TabHeaderSkin) node;

                double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));

                if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) {
                    selectedTabOffset = offset;
                    selectedTabWidth = tabHeaderPrefWidth;
                }
                offset += tabHeaderPrefWidth;
            }

            final double scrollOffset = getScrollOffset();
            final double selectedTabStartX = selectedTabOffset;
            final double selectedTabEndX = selectedTabOffset + selectedTabWidth;

            final double visibleAreaEndX = visibleWidth;

            if (selectedTabStartX < -scrollOffset) {
                setScrollOffset(-selectedTabStartX);
            } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) {
                setScrollOffset(visibleAreaEndX - selectedTabEndX);
            }
        }

        public double getScrollOffset() {
            return scrollOffset;
        }

        private void validateScrollOffset() {
            setScrollOffset(getScrollOffset());
        }

        private void setScrollOffset(double newScrollOffset) {
            double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight());
            double controlTabWidth = snapSize(controlButtons.getWidth());
            double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER;

            double offset = 0.0;
            for (Node node : headersRegion.getChildren()) {
                TabHeaderSkin tabHeader = (TabHeaderSkin) node;
                double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
                offset += tabHeaderPrefWidth;
            }

            double actualNewScrollOffset;

            if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) {
                actualNewScrollOffset = visibleWidth - offset;
            } else if (newScrollOffset > 0) {
                actualNewScrollOffset = 0;
            } else {
                actualNewScrollOffset = newScrollOffset;
            }

            if (actualNewScrollOffset != scrollOffset) {
                scrollOffset = actualNewScrollOffset;
                headersRegion.requestLayout();
            }
        }

        private double firstTabIndent() {
            switch (getSkinnable().getSide()) {
                case TOP:
                case BOTTOM:
                    return snappedLeftInset();
                case RIGHT:
                case LEFT:
                    return snappedTopInset();
                default:
                    return 0;
            }
        }

        @Override
        protected double computePrefWidth(double height) {
            double padding = isHorizontal() ? snappedLeftInset() + snappedRightInset() : snappedTopInset() + snappedBottomInset();
            return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) + firstTabIndent() + SPACER + padding;
        }

        @Override
        protected double computePrefHeight(double width) {
            double padding = isHorizontal() ? snappedTopInset() + snappedBottomInset() : snappedLeftInset() + snappedRightInset();
            return snapSize(headersRegion.prefHeight(-1)) + padding;
        }

        @Override
        public double getBaselineOffset() {
            if (getSkinnable().getSide() == Side.TOP) {
                return headersRegion.getBaselineOffset() + snappedTopInset();
            }
            return 0;
        }

        @Override
        protected void layoutChildren() {
            final double leftInset = snappedLeftInset();
            final double rightInset = snappedRightInset();
            final double topInset = snappedTopInset();
            final double bottomInset = snappedBottomInset();
            double w = snapSize(getWidth()) - (isHorizontal() ? leftInset + rightInset : topInset + bottomInset);
            double h = snapSize(getHeight()) - (isHorizontal() ? topInset + bottomInset : leftInset + rightInset);
            double tabBackgroundHeight = snapSize(prefHeight(-1));
            double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
            double headersPrefHeight = snapSize(headersRegion.prefHeight(-1));

            controlButtons.showTabsMenu(!tabsFit());

            updateHeaderClip();
            headersRegion.requestLayout();

            double btnWidth = snapSize(controlButtons.prefWidth(-1));
            final double btnHeight = controlButtons.prefHeight(btnWidth);
            controlButtons.resize(btnWidth, btnHeight);

            headersRegion.resize(headersPrefWidth, headersPrefHeight);

            if (isFloatingStyleClass()) {
                headerBackground.setVisible(false);
            } else {
                headerBackground.resize(snapSize(getWidth()), snapSize(getHeight()));
                headerBackground.setVisible(true);
            }

            double startX = 0;
            double startY = 0;
            double controlStartX = 0;
            double controlStartY = 0;
            Side tabPosition = getSkinnable().getSide();

            if (tabPosition.equals(Side.TOP)) {
                startX = leftInset;
                startY = tabBackgroundHeight - headersPrefHeight - bottomInset;
                controlStartX = w - btnWidth + leftInset;
                controlStartY = snapSize(getHeight()) - btnHeight - bottomInset;
            } else if (tabPosition.equals(Side.RIGHT)) {
                startX = topInset;
                startY = tabBackgroundHeight - headersPrefHeight - leftInset;
                controlStartX = w - btnWidth + topInset;
                controlStartY = snapSize(getHeight()) - btnHeight - leftInset;
            } else if (tabPosition.equals(Side.BOTTOM)) {
                startX = snapSize(getWidth()) - headersPrefWidth - leftInset;
                startY = tabBackgroundHeight - headersPrefHeight - topInset;
                controlStartX = rightInset;
                controlStartY = snapSize(getHeight()) - btnHeight - topInset;
            } else if (tabPosition.equals(Side.LEFT)) {
                startX = snapSize(getWidth()) - headersPrefWidth - topInset;
                startY = tabBackgroundHeight - headersPrefHeight - rightInset;
                controlStartX = leftInset;
                controlStartY = snapSize(getHeight()) - btnHeight - rightInset;
            }
            if (headerBackground.isVisible()) {
                positionInArea(headerBackground, 0, 0, snapSize(getWidth()), snapSize(getHeight()), 0, HPos.CENTER, VPos.CENTER);
            }
            positionInArea(headersRegion, startX, startY, w, h, 0, HPos.LEFT, VPos.CENTER);
            positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight, 0, HPos.CENTER, VPos.CENTER);
        }
    }

    static int CLOSE_BTN_SIZE = 16;

    class TabHeaderSkin extends StackPane {
        private final Tab tab;

        public Tab getTab() {
            return tab;
        }

        private Label label;
        private StackPane closeBtn;
        private StackPane inner;
        private Tooltip oldTooltip;
        private Tooltip tooltip;
        private Rectangle clip;

        private boolean isClosing = false;

        private MultiplePropertyChangeListenerHandler listener = new MultiplePropertyChangeListenerHandler(param ->
        {
            handlePropertyChanged(param);
            return null;
        });

        private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() {
            @Override
            public void onChanged(Change<? extends String> c) {
                getStyleClass().setAll(tab.getStyleClass());
            }
        };

        private final WeakListChangeListener<String> weakStyleClassListener = new WeakListChangeListener<>(styleClassListener);

        public TabHeaderSkin(final Tab tab) {
            getStyleClass().setAll(tab.getStyleClass());
            setId(tab.getId());
            setStyle(tab.getStyle());
            setAccessibleRole(AccessibleRole.TAB_ITEM);

            this.tab = tab;
            clip = new Rectangle();
            setClip(clip);

            label = new Label(tab.getText(), tab.getGraphic());
            label.getStyleClass().setAll("tab-label");

            closeBtn = new StackPane() {
                @Override
                protected double computePrefWidth(double h) {
                    return CLOSE_BTN_SIZE;
                }

                @Override
                protected double computePrefHeight(double w) {
                    return CLOSE_BTN_SIZE;
                }

                @Override
                public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
                    switch (action) {
                        case FIRE: {
                            Tab tab = getTab();
                            TabPaneBehavior behavior = getBehavior();
                            if (behavior.canCloseTab(tab)) {
                                behavior.closeTab(tab);
                                setOnMousePressed(null);
                            }
                        }
                        default:
                            super.executeAccessibleAction(action, parameters);
                    }
                }
            };
            closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
            closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
            closeBtn.getStyleClass().setAll("tab-close-button");
            closeBtn.setOnMousePressed(me -> {
                Tab tab1 = getTab();
                TabPaneBehavior behavior = getBehavior();
                if (behavior.canCloseTab(tab1)) {
                    behavior.closeTab(tab1);
                    setOnMousePressed(null);
                }
            });

            updateGraphicRotation();

            final Region focusIndicator = new Region();
            focusIndicator.setMouseTransparent(true);
            focusIndicator.getStyleClass().add("focus-indicator");

            inner = new StackPane() {
                @Override
                protected void layoutChildren() {
                    final TabPane skinnable = getSkinnable();

                    final double paddingTop = snappedTopInset();
                    final double paddingRight = snappedRightInset();
                    final double paddingBottom = snappedBottomInset();
                    final double paddingLeft = snappedLeftInset();
                    final double w = getWidth() - (paddingLeft + paddingRight);
                    final double h = getHeight() - (paddingTop + paddingBottom);

                    final double prefLabelWidth = snapSize(label.prefWidth(-1));
                    final double prefLabelHeight = snapSize(label.prefHeight(-1));

                    final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0;
                    final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0;
                    final double minWidth = snapSize(skinnable.getTabMinWidth());
                    final double maxWidth = snapSize(skinnable.getTabMaxWidth());
                    final double maxHeight = snapSize(skinnable.getTabMaxHeight());

                    double labelAreaWidth = prefLabelWidth;
                    double labelWidth = prefLabelWidth;
                    double labelHeight = prefLabelHeight;

                    final double childrenWidth = labelAreaWidth + closeBtnWidth;
                    final double childrenHeight = Math.max(labelHeight, closeBtnHeight);

                    if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) {
                        labelAreaWidth = maxWidth - closeBtnWidth;
                        labelWidth = maxWidth - closeBtnWidth;
                    } else if (childrenWidth < minWidth) {
                        labelAreaWidth = minWidth - closeBtnWidth;
                    }

                    if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) {
                        labelHeight = maxHeight;
                    }

                    if (animationState != TabAnimationState.NONE) {
//                        if (prefWidth.getValue() < labelAreaWidth) {
//                            labelAreaWidth = prefWidth.getValue();
//                        }
                        labelAreaWidth *= animationTransition.get();
                        closeBtn.setVisible(false);
                    } else {
                        closeBtn.setVisible(showCloseButton());
                    }

                    label.resize(labelWidth, labelHeight);

                    double labelStartX = paddingLeft;
                    double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth;

                    positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h, 0, HPos.CENTER, VPos.CENTER);

                    if (closeBtn.isVisible()) {
                        closeBtn.resize(closeBtnWidth, closeBtnHeight);
                        positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h, 0, HPos.CENTER, VPos.CENTER);
                    }

                    final int vPadding = Utils.isMac() ? 2 : 3;
                    final int hPadding = Utils.isMac() ? 2 : 1;
                    focusIndicator.resizeRelocate(paddingLeft - hPadding, paddingTop + vPadding, w + 2 * hPadding, h - 2 * vPadding);
                }
            };
            inner.getStyleClass().add("tab-container");
            inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
            inner.getChildren().addAll(label, closeBtn, focusIndicator);

            getChildren().addAll(inner);

            tooltip = tab.getTooltip();
            if (tooltip != null) {
                Tooltip.install(this, tooltip);
                oldTooltip = tooltip;
            }

            listener.registerChangeListener(tab.closableProperty(), "CLOSABLE");
            listener.registerChangeListener(tab.selectedProperty(), "SELECTED");
            listener.registerChangeListener(tab.textProperty(), "TEXT");
            listener.registerChangeListener(tab.graphicProperty(), "GRAPHIC");
            listener.registerChangeListener(tab.contextMenuProperty(), "CONTEXT_MENU");
            listener.registerChangeListener(tab.tooltipProperty(), "TOOLTIP");
            listener.registerChangeListener(tab.disableProperty(), "DISABLE");
            listener.registerChangeListener(tab.styleProperty(), "STYLE");

            tab.getStyleClass().addListener(weakStyleClassListener);

            listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(), "TAB_CLOSING_POLICY");
            listener.registerChangeListener(getSkinnable().sideProperty(), "SIDE");
            listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), "ROTATE_GRAPHIC");
            listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), "TAB_MIN_WIDTH");
            listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), "TAB_MAX_WIDTH");
            listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), "TAB_MIN_HEIGHT");
            listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), "TAB_MAX_HEIGHT");

            getProperties().put(Tab.class, tab);
            getProperties().put(ContextMenu.class, tab.getContextMenu());

            setOnContextMenuRequested((ContextMenuEvent me) ->
            {
                if (getTab().getContextMenu() != null) {
                    getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
                    me.consume();
                }
            });
            setOnMousePressed(me -> {
                if (getTab().isDisable()) {
                    return;
                }
                if (me.getButton().equals(MouseButton.MIDDLE)) {
                    if (showCloseButton()) {
                        Tab tab12 = getTab();
                        TabPaneBehavior behavior = getBehavior();
                        if (behavior.canCloseTab(tab12)) {
                            removeListeners(tab12);
                            behavior.closeTab(tab12);
                        }
                    }
                } else if (me.getButton().equals(MouseButton.PRIMARY)) {
                    getBehavior().selectTab(getTab());
                }
            });

            pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
            pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
            final Side side = getSkinnable().getSide();
            pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
            pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
            pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
            pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
        }

        private void handlePropertyChanged(final String p) {
            if ("CLOSABLE".equals(p)) {
                inner.requestLayout();
                requestLayout();
            } else if ("SELECTED".equals(p)) {
                pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
                inner.requestLayout();
                requestLayout();
            } else if ("TEXT".equals(p)) {
                label.setText(getTab().getText());
            } else if ("GRAPHIC".equals(p)) {
                label.setGraphic(getTab().getGraphic());
            } else if ("CONTEXT_MENU".equals(p)) {
            } else if ("TOOLTIP".equals(p)) {
                if (oldTooltip != null) {
                    Tooltip.uninstall(this, oldTooltip);
                }
                tooltip = tab.getTooltip();
                if (tooltip != null) {
                    Tooltip.install(this, tooltip);
                    oldTooltip = tooltip;
                }
            } else if ("DISABLE".equals(p)) {
                pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
                inner.requestLayout();
                requestLayout();
            } else if ("STYLE".equals(p)) {
                setStyle(tab.getStyle());
            } else if ("TAB_CLOSING_POLICY".equals(p)) {
                inner.requestLayout();
                requestLayout();
            } else if ("SIDE".equals(p)) {
                final Side side = getSkinnable().getSide();
                pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
                pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
                pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
                pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
                inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
                if (getSkinnable().isRotateGraphic()) {
                    updateGraphicRotation();
                }
            } else if ("ROTATE_GRAPHIC".equals(p)) {
                updateGraphicRotation();
            } else if ("TAB_MIN_WIDTH".equals(p)) {
                requestLayout();
                getSkinnable().requestLayout();
            } else if ("TAB_MAX_WIDTH".equals(p)) {
                requestLayout();
                getSkinnable().requestLayout();
            } else if ("TAB_MIN_HEIGHT".equals(p)) {
                requestLayout();
                getSkinnable().requestLayout();
            } else if ("TAB_MAX_HEIGHT".equals(p)) {
                requestLayout();
                getSkinnable().requestLayout();
            }
        }

        private void updateGraphicRotation() {
            if (label.getGraphic() != null) {
                label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F : (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F : (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F)));
            }
        }

        private boolean showCloseButton() {
            return tab.isClosable() && (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) || getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected());
        }

        private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) {
            @Override
            protected void invalidated() {
                requestLayout();
            }
        };

        private void removeListeners(Tab tab) {
            listener.dispose();
            inner.getChildren().clear();
            getChildren().clear();
        }

        private TabAnimationState animationState = TabAnimationState.NONE;
        private Timeline currentAnimation;

        @Override
        protected double computePrefWidth(double height) {
//            if (animating) {
//                return prefWidth.getValue();
//            }
            double minWidth = snapSize(getSkinnable().getTabMinWidth());
            double maxWidth = snapSize(getSkinnable().getTabMaxWidth());
            double paddingRight = snappedRightInset();
            double paddingLeft = snappedLeftInset();
            double tmpPrefWidth = snapSize(label.prefWidth(-1));

            if (showCloseButton()) {
                tmpPrefWidth += snapSize(closeBtn.prefWidth(-1));
            }

            if (tmpPrefWidth > maxWidth) {
                tmpPrefWidth = maxWidth;
            } else if (tmpPrefWidth < minWidth) {
                tmpPrefWidth = minWidth;
            }
            tmpPrefWidth += paddingRight + paddingLeft;
//            prefWidth.setValue(tmpPrefWidth);
            return tmpPrefWidth;
        }

        @Override
        protected double computePrefHeight(double width) {
            double minHeight = snapSize(getSkinnable().getTabMinHeight());
            double maxHeight = snapSize(getSkinnable().getTabMaxHeight());
            double paddingTop = snappedTopInset();
            double paddingBottom = snappedBottomInset();
            double tmpPrefHeight = snapSize(label.prefHeight(width));

            if (tmpPrefHeight > maxHeight) {
                tmpPrefHeight = maxHeight;
            } else if (tmpPrefHeight < minHeight) {
                tmpPrefHeight = minHeight;
            }
            tmpPrefHeight += paddingTop + paddingBottom;
            return tmpPrefHeight;
        }

        @Override
        protected void layoutChildren() {
            double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue();
            inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset());
            inner.relocate(snappedLeftInset(), snappedTopInset());
        }

        @Override
        protected void setWidth(double value) {
            super.setWidth(value);
            clip.setWidth(value);
        }

        @Override
        protected void setHeight(double value) {
            super.setHeight(value);
            clip.setHeight(value);
        }

        @Override
        public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
            switch (attribute) {
                case TEXT:
                    return getTab().getText();
                case SELECTED:
                    return selectedTab == getTab();
                default:
                    return super.queryAccessibleAttribute(attribute, parameters);
            }
        }

        @Override
        public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
            switch (action) {
                case REQUEST_FOCUS:
                    getSkinnable().getSelectionModel().select(getTab());
                    break;
                default:
                    super.executeAccessibleAction(action, parameters);
            }
        }
    }

    private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("selected");
    private static final PseudoClass TOP_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("top");
    private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("bottom");
    private static final PseudoClass LEFT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("left");
    private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("right");
    private static final PseudoClass DISABLED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("disabled");

    class TabContentRegion extends StackPane {

        private TraversalEngine engine;
        private Direction direction = Direction.NEXT;
        private Tab tab;

        private InvalidationListener tabContentListener = valueModel -> updateContent();
        private InvalidationListener tabSelectedListener = new InvalidationListener() {
            @Override
            public void invalidated(Observable valueModel) {
                setVisible(tab.isSelected());
            }
        };

        private WeakInvalidationListener weakTabContentListener = new WeakInvalidationListener(tabContentListener);
        private WeakInvalidationListener weakTabSelectedListener = new WeakInvalidationListener(tabSelectedListener);

        public Tab getTab() {
            return tab;
        }

        public TabContentRegion(Tab tab) {
            getStyleClass().setAll("tab-content-area");
            setManaged(false);
            this.tab = tab;
            updateContent();
            setVisible(tab.isSelected());

            tab.selectedProperty().addListener(weakTabSelectedListener);
            tab.contentProperty().addListener(weakTabContentListener);
        }

        private void updateContent() {
            Node newContent = getTab().getContent();
            if (newContent == null) {
                getChildren().clear();
            } else {
                getChildren().setAll(newContent);
            }
        }

        private void removeListeners(Tab tab) {
            tab.selectedProperty().removeListener(weakTabSelectedListener);
            tab.contentProperty().removeListener(weakTabContentListener);
        }
    }

    class TabControlButtons extends StackPane {
        private StackPane inner;
        private StackPane downArrow;
        private Pane downArrowBtn;
        private boolean showControlButtons;
        private ContextMenu popup;

        public TabControlButtons() {
            getStyleClass().setAll("control-buttons-tab");

            TabPane tabPane = getSkinnable();

            downArrowBtn = new Pane();
            downArrowBtn.getStyleClass().setAll("tab-down-button");
            downArrowBtn.setVisible(isShowTabsMenu());
            downArrow = new StackPane();
            downArrow.setManaged(false);
            downArrow.getStyleClass().setAll("arrow");
            downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
            downArrowBtn.getChildren().add(downArrow);
            downArrowBtn.setOnMouseClicked(me -> showPopupMenu());

            setupPopupMenu();

            inner = new StackPane() {
                @Override
                protected double computePrefWidth(double height) {
                    double pw;
                    double maxArrowWidth = !isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight()));
                    pw = 0.0F;
                    if (isShowTabsMenu()) {
                        pw += maxArrowWidth;
                    }
                    if (pw > 0) {
                        pw += snappedLeftInset() + snappedRightInset();
                    }
                    return pw;
                }

                @Override
                protected double computePrefHeight(double width) {
                    double height = 0.0F;
                    if (isShowTabsMenu()) {
                        height = Math.max(height, snapSize(downArrowBtn.prefHeight(width)));
                    }
                    if (height > 0) {
                        height += snappedTopInset() + snappedBottomInset();
                    }
                    return height;
                }

                @Override
                protected void layoutChildren() {
                    if (isShowTabsMenu()) {
                        double x = 0;
                        double y = snappedTopInset();
                        double w = snapSize(getWidth()) - x + snappedLeftInset();
                        double h = snapSize(getHeight()) - y + snappedBottomInset();
                        positionArrow(downArrowBtn, downArrow, x, y, w, h);
                    }
                }

                private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) {
                    btn.resize(width, height);
                    positionInArea(btn, x, y, width, height, 0, HPos.CENTER, VPos.CENTER);
                    double arrowWidth = snapSize(arrow.prefWidth(-1));
                    double arrowHeight = snapSize(arrow.prefHeight(-1));
                    arrow.resize(arrowWidth, arrowHeight);
                    positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(), width - btn.snappedLeftInset() - btn.snappedRightInset(), height - btn.snappedTopInset() - btn.snappedBottomInset(), 0, HPos.CENTER, VPos.CENTER);
                }
            };
            inner.getStyleClass().add("container");
            inner.getChildren().add(downArrowBtn);

            getChildren().add(inner);

            tabPane.sideProperty().addListener(valueModel ->
            {
                Side tabPosition = getSkinnable().getSide();
                downArrow.setRotate(tabPosition.equals(Side.BOTTOM) ? 180.0F : 0.0F);
            });
            tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu());
            showControlButtons = false;
            if (isShowTabsMenu()) {
                showControlButtons = true;
                requestLayout();
            }
            getProperties().put(ContextMenu.class, popup);
        }

        private boolean showTabsMenu = false;

        private void showTabsMenu(boolean value) {
            final boolean wasTabsMenuShowing = isShowTabsMenu();
            this.showTabsMenu = value;

            if (showTabsMenu && !wasTabsMenuShowing) {
                downArrowBtn.setVisible(true);
                showControlButtons = true;
                inner.requestLayout();
                tabHeaderArea.requestLayout();
            } else if (!showTabsMenu && wasTabsMenuShowing) {
                hideControlButtons();
            }
        }

        private boolean isShowTabsMenu() {
            return showTabsMenu;
        }

        @Override
        protected double computePrefWidth(double height) {
            double pw = snapSize(inner.prefWidth(height));
            if (pw > 0) {
                pw += snappedLeftInset() + snappedRightInset();
            }
            return pw;
        }

        @Override
        protected double computePrefHeight(double width) {
            return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) + snappedTopInset() + snappedBottomInset();
        }

        @Override
        protected void layoutChildren() {
            double x = snappedLeftInset();
            double y = snappedTopInset();
            double w = snapSize(getWidth()) - x + snappedRightInset();
            double h = snapSize(getHeight()) - y + snappedBottomInset();

            if (showControlButtons) {
                showControlButtons();
                showControlButtons = false;
            }

            inner.resize(w, h);
            positionInArea(inner, x, y, w, h, 0, HPos.CENTER, VPos.BOTTOM);
        }

        private void showControlButtons() {
            setVisible(true);
            if (popup == null) {
                setupPopupMenu();
            }
        }

        private void hideControlButtons() {
            if (isShowTabsMenu()) {
                showControlButtons = true;
            } else {
                setVisible(false);
                popup.getItems().clear();
                popup = null;
            }
            requestLayout();
        }

        private void setupPopupMenu() {
            if (popup == null) {
                popup = new ContextMenu();
            }
            popup.getItems().clear();
            ToggleGroup group = new ToggleGroup();
            ObservableList<RadioMenuItem> menuitems = FXCollections.observableArrayList();
            for (final Tab tab : getSkinnable().getTabs()) {
                TabMenuItem item = new TabMenuItem(tab);
                item.setToggleGroup(group);
                item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab));
                menuitems.add(item);
            }
            popup.getItems().addAll(menuitems);
        }

        private void showPopupMenu() {
            for (MenuItem mi : popup.getItems()) {
                TabMenuItem tmi = (TabMenuItem) mi;
                if (selectedTab.equals(tmi.getTab())) {
                    tmi.setSelected(true);
                    break;
                }
            }
            popup.show(downArrowBtn, Side.BOTTOM, 0, 0);
        }
    }

    class TabMenuItem extends RadioMenuItem {
        DockNodeTab tab;

        private InvalidationListener disableListener = new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                setDisable(tab.isDisable());
            }
        };

        private WeakInvalidationListener weakDisableListener =
                new WeakInvalidationListener(disableListener);

        public TabMenuItem(final Tab tab) {
            this((DockNodeTab) tab);
        }

        public TabMenuItem(final DockNodeTab tab) {
            super(tab.getTitle(), ContentTabPaneSkin.clone(tab.getGraphic()));
            this.tab = tab;
            setDisable(tab.isDisable());
            tab.disableProperty().addListener(weakDisableListener);
            textProperty().bind(tab.titleProperty());
        }

        public Tab getTab() {
            return tab;
        }

        public void dispose() {
            tab.disableProperty().removeListener(weakDisableListener);
        }
    }

    @Override
    public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
        switch (attribute) {
            case FOCUS_ITEM:
                return tabHeaderArea.getTabHeaderSkin(selectedTab);
            case ITEM_COUNT:
                return tabHeaderArea.headersRegion.getChildren().size();
            case ITEM_AT_INDEX: {
                Integer index = (Integer) parameters[0];
                if (index == null) {
                    return null;
                }
                return tabHeaderArea.headersRegion.getChildren().get(index);
            }
            default:
                return super.queryAccessibleAttribute(attribute, parameters);
        }
    }
}
